/*
 * Copyright (c) 2018, apexes.net. All rights reserved.
 *
 *         http://www.apexes.net
 *
 */
package net.apexes.commons.net;

import net.apexes.commons.lang.Checks;
import net.apexes.commons.lang.Networks;
import net.apexes.commons.lang.Streams;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * 
 * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
 *
 */
public class StringHttpClient {

    public static final String CONTENT_TYPE_APPLICATION_JSON = "application/json";
    public static final String CONTENT_TYPE_APPLICATION_OCTETSTREAM = "application/octet-stream";

    public static final String METHOD_POST = "POST";
    public static final String METHOD_GET = "GET";
    public static final String METHOD_PUT = "PUT";
    public static final String METHOD_DELETE = "DELETE";

    public static final String GZIP = "gzip";

    private static final RequestEncoder DEFAULT_REQUEST_ENCODER = new SimpleRequestEncoder();

    private final String url;
    private final Map<String, String> requestHeaders;
    private final String contentType;
    private Charset charset = StandardCharsets.UTF_8;
    private int connectTimeout = 3 * 1000;
    private int readTimeout = 30 * 1000;
    private boolean acceptCompress;
    private int successfulResponseCode = 200;
    private SSLSocketFactory sslSocketFactory;
    private HostnameVerifier hostnameVerifier;
    private RequestLogger sentLogger;
    private RequestLogger requestLogger;
    private ResponseLogger responseLogger;
    private RequestEncoder requestEncoder;
    private ResponseReader responseReader;

    public StringHttpClient(String url) {
        this(url, CONTENT_TYPE_APPLICATION_JSON);
    }

    public StringHttpClient(String url, String contentType) {
        Checks.verifyNotNull(url, "url");
        Checks.verifyNotNull(contentType, "contentType");
        this.url = url;
        this.contentType = contentType;
        this.requestHeaders = new LinkedHashMap<>();
    }

    public String getUrl() {
        return url;
    }

    public Charset getCharset() {
        return charset;
    }

    public void setCharset(Charset charset) {
        Checks.verifyNotNull(charset, "charset");
        this.charset = charset;
    }

    public void setCharset(String charset) {
        Checks.verifyNotNull(charset, "charset");
        this.charset = Charset.forName(charset);
    }

    public boolean isAcceptCompress() {
        return acceptCompress;
    }

    public StringHttpClient setAcceptCompress(boolean value) {
        acceptCompress = value;
        return this;
    }

    public int getSuccessfulResponseCode() {
        return successfulResponseCode;
    }

    public StringHttpClient setSuccessfulResponseCode(int code) {
        this.successfulResponseCode = code;
        return this;
    }
    
    /**
     * 设置连接超时时间
     * @param timeout 超时时间，单位ms
     */
    public StringHttpClient setConnectTimeout(int timeout) {
        this.connectTimeout = timeout;
        return this;
    }
    
    /**
     * 设置读取超时时间
     * @param timeout 超时时间，单位ms
     */
    public StringHttpClient setReadTimeout(int timeout) {
        this.readTimeout = timeout;
        return this;
    }
    
    public StringHttpClient setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
        this.sslSocketFactory = sslSocketFactory;
        return this;
    }
    
    public StringHttpClient setHostnameVerifier(HostnameVerifier hostnameVerifier) {
        this.hostnameVerifier = hostnameVerifier;
        return this;
    }

    public StringHttpClient exemptSSL() throws KeyManagementException, NoSuchAlgorithmException {
        this.sslSocketFactory = Networks.exemptSSLContext().getSocketFactory();
        this.hostnameVerifier = Networks.exemptHostnameVerifier();
        return this;
    }

    public StringHttpClient setRequestHeader(String key, String value) {
        Checks.verifyNotNull(key, "key");
        Checks.verifyNotNull(value, "value");
        requestHeaders.put(key, value);
        return this;
    }

    /**
     * @deprecated 使用 {@link #setRequestHeader(String, String)}
     */
    @Deprecated
    public StringHttpClient setHttpProperty(String key, String value) {
        return setRequestHeader(key, value);
    }

    public String getRequestHeader(String key) {
        Checks.verifyNotNull(key, "key");
        return requestHeaders.get(key);
    }

    /**
     * @deprecated 使用 {@link #getRequestHeader(String)}
     */
    @Deprecated
    public String getHttpProperty(String key) {
        return getRequestHeader(key);
    }

    public StringHttpClient setSentLogger(RequestLogger sentLogger) {
        this.sentLogger = sentLogger;
        return this;
    }
    
    public StringHttpClient setRequestLogger(RequestLogger requestLogger) {
        this.requestLogger = requestLogger;
        return this;
    }
    
    public StringHttpClient setResponseLogger(ResponseLogger responseLogger) {
        this.responseLogger = responseLogger;
        return this;
    }

    public StringHttpClient setRequestEncoder(RequestEncoder requestEncoder) {
        this.requestEncoder = requestEncoder;
        return this;
    }
    
    public StringHttpClient setResponseReader(ResponseReader responseReader) {
        this.responseReader = responseReader;
        return this;
    }

    public String doPost(String request) throws Exception {
        return doPost(request, false);
    }

    public String doPost(String request, boolean compress) throws Exception {
        return call(METHOD_POST, request, compress);
    }

    public String doPut(String request) throws Exception {
        return doPut(request, false);
    }

    public String doPut(String request, boolean compress) throws Exception {
        return call(METHOD_PUT, request, compress);
    }

    private String call(String method, String request, boolean compress) throws Exception {
        byte[] body;
        if (requestEncoder != null) {
            body = requestEncoder.encode(this, request, compress);
        } else {
            body = DEFAULT_REQUEST_ENCODER.encode(this, request, compress);
        }
        HttpURLConnection conn = openConnection(method, new URL(url), compress);
        try {
            if (requestLogger != null) {
                requestLogger.log(requestHeader(), url, request);
            }
            if (body != null) {
                conn.getOutputStream().write(body);
            } else {
                conn.setRequestProperty("Content-Length", "0");
            }
            conn.getOutputStream().flush();
            conn.getOutputStream().close();
            if (sentLogger != null) {
                sentLogger.log(requestHeader(), url, request);
            }
            return handleResponse(conn);
        } finally {
            conn.disconnect();
        }
    }

    public String doGet() throws Exception {
        return call(METHOD_GET);
    }

    public String doDelete() throws Exception {
        return call(METHOD_DELETE);
    }

    private String call(String method) throws Exception {
        HttpURLConnection conn = openConnection(method, new URL(url), false);
        try {
            if (requestLogger != null) {
                requestLogger.log(requestHeader(), url, null);
            }
            conn.connect();
            if (sentLogger != null) {
                sentLogger.log(requestHeader(), url, null);
            }
            return handleResponse(conn);
        } finally {
            conn.disconnect();
        }
    }
    
    private String handleResponse(HttpURLConnection conn) throws Exception {
        int code = conn.getResponseCode();
        if (code != successfulResponseCode) {
            String message = conn.getResponseMessage();
            String errorMessage = null;
            if (code >= 400) {
                errorMessage = ResponseException.readErrorMessage(conn.getErrorStream(), charset);
            }
            ResponseException exception = new ResponseException(code, message, errorMessage);
            if (responseLogger != null) {
                responseLogger.log(responseHeader(conn), url, exception.getResponseCode(), exception.getResponseMessage());
            }
            throw exception;
        }
        InputStream ips;
        if (GZIP.equalsIgnoreCase(conn.getHeaderField("Content-Encoding"))) {
            ips = new GZIPInputStream(conn.getInputStream());
        } else {
            ips = conn.getInputStream();
        }
        if (responseReader == null) {
            responseReader = new SimpleResponseReader(charset);
        }
        String response = responseReader.read(conn::getHeaderField, ips);
        if (responseLogger != null) {
            responseLogger.log(responseHeader(conn), url, 200, response);
        }
        return response;
    }

    protected HttpURLConnection openConnection(String method, URL url, boolean compress) throws Exception {
        HttpURLConnection conn = Networks.connect(url, sslSocketFactory, hostnameVerifier);
        conn.setRequestMethod(method);
        conn.setDoOutput(true);
        conn.setDoInput(true);
        conn.setUseCaches(false);
        conn.setConnectTimeout(connectTimeout);
        conn.setReadTimeout(readTimeout);

        if (compress) {
            conn.setRequestProperty("Content-Encoding", GZIP);
            conn.setRequestProperty("Content-Type", CONTENT_TYPE_APPLICATION_OCTETSTREAM);
        } else {
            conn.setRequestProperty("Content-Type", contentType + "; charset=" + getCharset());
        }
        if (acceptCompress) {
            conn.setRequestProperty("Accept-Encoding", GZIP);
        }
        
        for (Map.Entry<String, String> entry : requestHeaders.entrySet()) {
            conn.setRequestProperty(entry.getKey(), entry.getValue());
        }

        return conn;
    }

    private Header requestHeader() {
        return requestHeaders::get;
    }

    private Header responseHeader(final HttpURLConnection conn) {
        return conn::getHeaderField;
    }
    
    public static StringHttpClient forRequest(String url) {
        return new StringHttpClient(url);
    }

    /**
     *
     * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
     */
    public interface RequestEncoder {
        byte[] encode(StringHttpClient client, String request, boolean compress) throws Exception;
    }

    /**
     *
     * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
     */
    public static class SimpleRequestEncoder implements RequestEncoder {

        @Override
        public byte[] encode(StringHttpClient client, String request, boolean compress) throws Exception {
            if (request == null) {
                return null;
            }

            Charset charset = client.getCharset();
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            if (compress) {
                GZIPOutputStream gzipOut = new GZIPOutputStream(out);
                gzipOut.write(request.getBytes(charset));
                gzipOut.finish();
                gzipOut.close();
            } else {
                out.write(request.getBytes(charset));
            }
            return out.toByteArray();
        }
    }

    /**
     *
     * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
     */
    public static class SimpleResponseReader implements ResponseReader {

        private final Charset charset;

        public SimpleResponseReader() {
            this(StandardCharsets.UTF_8);
        }

        public SimpleResponseReader(Charset charset) {
            this.charset = charset;
        }

        @Override
        public String read(Header header, InputStream is) throws Exception {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Streams.transfer(is, os);
            return new String(os.toByteArray(), charset);
        }
    }

    /**
     *
     * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
     */
    public interface ResponseReader {
        String read(Header header, InputStream ips) throws Exception;
    }

    public interface Header {
        String get(String key);
    }

    /**
     *
     * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
     */
    public interface RequestLogger {
        void log(Header header, String url, String text);
    }

    public interface ResponseLogger {
        void log(Header header, String url, int status, String text);
    }

}
